import type { useAsyncState } from "@vueuse/core";
import { v4 as uuidv4 } from "uuid";
import { inject, ref, type Ref } from "vue";

/**
 * {@link useAsyncState}
 */
namespace UseAsyncRef {
  export interface Options<T> {
    supplier: () => Promise<T>;
    onSuccess?: (t: T) => void;
    onError?: (error: any) => void;
    throwError?: boolean;
    resetOnExecute?: boolean;
  }
  export type State = "new" | "loading" | "error" | "ready";
  export type View<T> = {
    state: { [_ in State]: boolean };
    value: T | undefined;
    error: any;
  };
  export type ExecuteOptions<T> = {
    onState?: { [_ in State]?: boolean };
  } & { [K in keyof Options<T>]?: Options<T>[K] };
}
export function useAsyncRef<T extends object>(
  defaultOptions: UseAsyncRef.Options<T>
) {
  const state = ref<UseAsyncRef.State>("new");
  const value = ref<T>();
  const error = ref();
  let id = uuidv4();

  function view(): UseAsyncRef.View<T> {
    return {
      state: {
        new: state.value === "new",
        loading: state.value === "loading",
        error: state.value === "error",
        ready: state.value === "ready",
      },
      value: value.value,
      error: error.value,
    };
  }

  function reset() {
    state.value = "new";
    value.value = undefined;
    error.value = undefined;
    id = uuidv4();
  }

  async function execute(options: UseAsyncRef.ExecuteOptions<T> = {}) {
    for (const [key, value] of Object.entries(options.onState ?? {})) {
      if ((state.value === key) !== value) {
        return;
      }
    }
    state.value = "loading";
    if (options.resetOnExecute ?? defaultOptions.resetOnExecute ?? false) {
      value.value = undefined;
    }
    error.value = undefined;
    const newId = uuidv4();
    id = newId;
    try {
      const newValue = await (options.supplier ?? defaultOptions.supplier)();
      if (newId === id) {
        state.value = "ready";
        value.value = newValue;
        (options.onSuccess ?? defaultOptions.onSuccess)?.(newValue);
      }
    } catch (reason) {
      if (newId === id) {
        state.value = "error";
        error.value = reason;
        const onError = options.onError ?? defaultOptions.onError;
        const throwError =
          options.throwError ??
          defaultOptions.throwError ??
          onError === undefined;
        onError?.(reason);
        if (throwError) {
          throw reason;
        }
      }
    }
  }

  return { state, value, error, view, reset, execute };
}

export function useContentSize() {
  return inject<{ width: Ref<number>; height: Ref<number> }>("contentSize")!;
}
